package com.nervenets.general.jwt;

import cn.hutool.core.util.ArrayUtil;
import com.nervenets.general.Global;
import com.nervenets.general.annotation.ProhibitDuplicateRequest;
import com.nervenets.general.annotation.RateLimiter;
import com.nervenets.general.annotation.RequestEncrypt;
import com.nervenets.general.config.ApplicationProperties;
import com.nervenets.general.exception.LogicException;
import com.nervenets.general.exception.TokenIllegalException;
import com.nervenets.general.jwt.aspect.JwtSecurity;
import com.nervenets.general.jwt.util.JwtUtils;
import com.nervenets.general.model.SecurityUser;
import com.nervenets.general.service.GlobalSecurityService;
import com.nervenets.general.service.RedisService;
import com.nervenets.general.utils.AESUtils;
import com.nervenets.general.utils.HttpTools;
import com.nervenets.general.utils.StringUtils;
import com.nervenets.general.wapper.NerveNetsHttpServletRequestWrapper;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.DigestUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Objects;

import static com.nervenets.general.Global.TokenIllegalType.*;
import static com.nervenets.general.i18n.I18nTranslator.braceTranslate;
import static com.nervenets.general.i18n.I18nTranslator.translate;
import static com.nervenets.general.redis.RedisMultiPointLocker.lockAndConflict;

@AllArgsConstructor
@Slf4j
public class AuthenticationInterceptor implements HandlerInterceptor {
    private final ApplicationProperties applicationProperties;
    private final GlobalSecurityService globalSecurityService;
    private final RedisService redisService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object) throws Exception {
        // 如果不是映射到方法的直接通过
        if (!(object instanceof HandlerMethod)) {
            return true;
        }
        final HandlerMethod handlerMethod = (HandlerMethod) object;
        final Method method = handlerMethod.getMethod();
        final Class<?> beanType = handlerMethod.getBeanType();

        String license = JwtUtils.license(request, applicationProperties.isTokenValidateDevice(), applicationProperties.isTokenValidateIp());
        if (method.isAnnotationPresent(ProhibitDuplicateRequest.class)) {
            ProhibitDuplicateRequest duplicateRequest = method.getAnnotation(ProhibitDuplicateRequest.class);
            lockAndConflict(StringUtils.formatKey(Global.Constants.RATE_LIMITER_KEY, DigestUtils.md5DigestAsHex(handlerMethod.toString().getBytes()), license, request.getSession().getId()), duplicateRequest.lockTime());
        }

        if (method.isAnnotationPresent(RateLimiter.class)) {
            RateLimiter limiter = method.getAnnotation(RateLimiter.class);
            String rateLimiterKey = null;
            switch (limiter.limitType()) {
                case DEFAULT:
                    rateLimiterKey = StringUtils.formatKey(Global.Constants.RATE_LIMITER_KEY, DigestUtils.md5DigestAsHex(handlerMethod.toString().getBytes()));
                    break;
                case IP:
                    rateLimiterKey = StringUtils.formatKey(Global.Constants.RATE_LIMITER_KEY, DigestUtils.md5DigestAsHex(handlerMethod.toString().getBytes()), HttpTools.getIpAddress(request));
                    break;
                case USER:
                    rateLimiterKey = StringUtils.formatKey(Global.Constants.RATE_LIMITER_KEY, DigestUtils.md5DigestAsHex(handlerMethod.toString().getBytes()), request.getSession().getId());
                    break;
            }
            boolean needSetExpired = !redisService.hasKey(rateLimiterKey) || redisService.getExpire(rateLimiterKey) < 0;
            Long total = redisService.incr(rateLimiterKey);
            if (needSetExpired) redisService.setExpire(rateLimiterKey, limiter.time());
            if (total > limiter.total()) {
                throw new LogicException((StringUtils.isBlank(limiter.message()) ? translate("application.exception.rate.limit") : braceTranslate(limiter.message())) + total);
            }
        }
        // 白名单链接直接放过
        if (ArrayUtil.contains(applicationProperties.getAuthorizePermitAll(), request.getRequestURI())) {
            verifyToken(request, license, false);
            return true;
        }

        JwtSecurity jwtSecurity = null;
        if (method.isAnnotationPresent(JwtSecurity.class)) {
            jwtSecurity = method.getAnnotation(JwtSecurity.class);
        }

        if (null != jwtSecurity && !jwtSecurity.required()) {
            verifyToken(request, license, false);
            return true;
        }

        verifyToken(request, license, true);

        //如果请求body为加密状态，解密之...
        if (!applicationProperties.isRequestEncrypt() && method.isAnnotationPresent(RequestEncrypt.class)) {
            if (request instanceof NerveNetsHttpServletRequestWrapper) {
                final NerveNetsHttpServletRequestWrapper wrapper = (NerveNetsHttpServletRequestWrapper) request;
                if (!wrapper.isEmptyBody()) {
                    wrapper.setBodyCopier(Objects.requireNonNull(AESUtils.decryptData(wrapper.getBody())).getBytes(StandardCharsets.UTF_8));
                }
            }
        }

        return true;
    }

    private SecurityUser verifyToken(HttpServletRequest request, String license, boolean required) throws Exception {
        String token = request.getHeader(Global.Constants.TOKEN_KEY);
        if (!StringUtils.isBlank(token)) {
            try {
                if (applicationProperties.isTokenEncrypt()) {
                    token = AESUtils.decryptData(token, new TokenIllegalException(translate("application.login.expired") + "-token decrypt expired", VERIFY));
                }
                if (!JwtUtils.verify(token, license)) {
                    throw new TokenIllegalException(translate("application.login.expired") + "-verify", VERIFY);
                }
                SecurityUser securityUser = globalSecurityService.getUserInfo(token, license);
                if (null == securityUser)
                    throw new TokenIllegalException(translate("application.login.expired") + "-none user", NONE_USER);
                log.info("鉴权信息 , token:{} , license:{}, userId:{}, userName:{}", token, license, securityUser.getId(), securityUser.getName());

                request.setAttribute(Global.Constants.SESSION_USER, securityUser);
                return securityUser;
            } catch (Exception e) {
                if (required) throw e;
                else return null;
            }
        }
        if (!required) return null;
        throw new TokenIllegalException(translate("application.login.expired") + "-no token", NONE_TOKEN);
    }
}
